// ==++==
// 
//   Copyright (c) 2008 Microsoft Corporation. All rights reserved.
//   License: MS-Pl
//
// ==--==

using System;
using System.Diagnostics;
using System.Reflection;
using Microsoft.ExtendedReflection.Metadata;
using Microsoft.ExtendedReflection.Metadata.Names;
using Microsoft.ExtendedReflection.Utilities.Safe;
using Microsoft.ExtendedReflection.Utilities.Safe.Diagnostics;
using Microsoft.ExtendedReflection.Emit;
using Microsoft.ExtendedReflection.Metadata.Builders;
using Microsoft.ExtendedReflection.Metadata.Interfaces;
using Microsoft.Pex.Engine.TestFrameworks;
using Microsoft.Pex.Engine.ComponentModel;
using Microsoft.Pex.Engine;
using Xunit;
using Microsoft.Pex.Framework.TestFrameworks;

namespace Pex.Xunit
{
    /// <summary>
    /// xUnit test framework
    /// </summary>
    public sealed class XunitTestFrameworkAttribute : PexTestFrameworkAttributeBase
    {
        public XunitTestFrameworkAttribute()
        {
            this.SetAsDefault = true;
        }

        protected override IPexTestFramework CreateTestFramework(IPexComponent host)
        {
            SafeDebug.AssumeNotNull(host, "host");

            return new TestFramework(host);
        }

        [Serializable]
        public class TestFramework : TestFrameworkBase
        {
            public TestFramework(IPexComponent host) : base(host)
            {
                testAttributeCtor = MetadataBuilderHelper.Attribute.ConstructorName(testAttribute.Definition, new TypeName[]{});
                testAttributeSkipProperty =
                            new PropertyDefinitionName(
                                frameworkAssembly, -1,
                                testAttribute.Definition, "Skip",
                            SystemTypes.String.SerializableName).SelfInstantiation;

                propertyAttributeCtor =
                            MetadataBuilderHelper.Attribute.ConstructorName(
                                propertyAttribute.Definition,
                                SystemTypes.String.SerializableName,
                                SystemTypes.String.SerializableName);
            }

            private readonly ShortAssemblyName frameworkAssembly = Metadata<FactAttribute>.Assembly.SerializableName;
            private readonly TypeName testAttribute = Metadata<FactAttribute>.SerializableName;
            private readonly TypeName assertException = Metadata<AssertException>.SerializableName;
            private readonly TypeName propertyAttribute = Metadata<TraitAttribute>.SerializableName;

            private readonly IMethod testAttributeCtor;
            private readonly IProperty testAttributeSkipProperty;
            private readonly IMethod propertyAttributeCtor;

            public override bool AttributeBased
            {
                get { return true; }
            }

            public override string Name
            {
                get { return "xUnit"; }
            }

            public override ShortAssemblyName AssemblyName
            {
                get { return this.frameworkAssembly; }
            }

            public override bool SupportsTestReflection
            {
                get { return true; }
            }

            public override void AddReferences(IManagedProject project)
            {
                SafeDebug.AssumeNotNull(project, "project");

                // xcopy deplay, no load hint
                project.AddReference(this.AssemblyName.Name, null, null);
            }

            public override bool SupportsPartialClasses
            {
                get { return true; }
            }

            public override void MarkTestClass(TypeDefinitionBuilder type)
            {
                SafeDebug.AssumeNotNull(type, "type");

                // xUnit does not have class level attributes
            }

            public override void MarkTestMethod(
                PexGeneratedTest test,
                MethodDefinitionBuilder method)
            {
                method.CustomAttributes.Add(new CustomAttributeBuilder(testAttributeCtor));
            }

            public override void MarkIgnored(MethodDefinitionBuilder method, string message)
            {
                // update TestAttribute with Skip = "reason"
                foreach (CustomAttributeBuilder ca in method.CustomAttributes)
                {
                    if (ca.Constructor.SerializableName == testAttribute)
                    {
                        ca.AddNamedArgument(testAttributeSkipProperty, MetadataExpression.String(message));
                        return;
                    }
                }

                SafeDebug.Fail("could not find test attribute");
            }

            public override void MarkExpectedException(VisibilityContext visibility, MethodDefinitionBuilder method, Exception exception)
            {
                // xunit does not have an attribute but 
                // it has a Assert.Throws(...); feature

                //This has bugs it which means it doesn't produce valid C#
                //MethodBodyBuilder body = MetadataBuilderHelper.WrapInExpectedException(
                //    visibility,
                //    method.MethodBodyBuilder,
                //    assertExceptionCtor,
                //    MetadataFromRuntimeHandles.GetObjectType(exception));

                //Might need to write old way (Catch == pass, else Assert.Fail)
                //MethodBodyBuilder body = new MethodBodyBuilder();
                //body.Comment(string.Format("Assert.Throws<{0}>(delegate", MetadataFromRuntimeHandles.GetObjectType(exception).ShortName));
                //body.Import(method.MethodBodyBuilder);
                //body.Comment(");");


                MethodBodyBuilder body = new MethodBodyBuilder();
                body.Comment("Ideally we should be using Assert.Throws.  This is a current limitation of Pex.xUnit.dll");
                body.BeginTryCatchFinally();

                body.BeginTry();
                //Import the old method call
                body.Import(method.MethodBodyBuilder);
                //Get the constructor
                TypeEx type = MetadataFromReflection.GetType(typeof(AssertException));
                Method constructor = type.GetVisibleDefaultInstanceConstructor(visibility);

                //Throw. The code should have throw the exception already, if it hasn't we want to fail.
                body.New(constructor);
                body.Throw();

                body.EndTry();

                body.BeginCatch(MetadataFromRuntimeHandles.GetObjectType(exception));
                //Do nothing, test will then pass.
                body.EndCatch();
                body.EndTryCatchFinally();

                // replace original with wraped 
                method.MethodBodyBuilder.SwapOperands(body);
            }

            public override void MakeInconclusive(MethodDefinitionBuilder method, string message)
            {
                MarkIgnored(method, message);
            }

            public override bool IsFixture(TypeDefinition target)
            {
                SafeDebug.AssumeNotNull(target, "target");

                // it's a fixture if it contains any tests
                foreach (MethodDefinition method in target.DeclaredInstanceMethods)
                    if (IsTest(method))
                        return true;

                return false;
            }

            public override bool IsTest(MethodDefinition method)
            {
                SafeDebug.AssumeNotNull(method, "method");

                return
                    VisibilityContext.Exported.CanSeeDefinition(method) && // public methods
                    !method.IsStatic && // instance methods
                    AttributeHelper.IsDefined(method, testAttribute); // with [TestAttribute]
            }

            public override bool TryReadExpectedException(ICustomAttributeProviderEx target, out TypeEx exceptionType)
            {
                SafeDebug.AssumeNotNull(target, "target");
                exceptionType = null;
                return false;
            }

            public override bool IsFixtureIgnored(
                TestFrameworkTestSelection testSelection,
                TypeEx fixtureType,
                out string ignoreMessage)
            {
                SafeDebug.AssumeNotNull(testSelection, "testSelection");
                SafeDebug.AssumeNotNull(fixtureType, "fixtureType");

                ignoreMessage = null;
                return false;
            }

            public override bool IsTestIgnored(
                TestFrameworkTestSelection testSelection,
                MethodDefinition method,
                out string ignoreMessage)
            {
                SafeDebug.AssumeNotNull(testSelection, "testSelection");
                SafeDebug.AssumeNotNull(method, "method");

                foreach (ICustomAttribute ca in method.DeclaredAttributes)
                {
                    if (ca.Constructor.SerializableName == testAttribute)
                    {
                        // if the skip named property is set, it's ignored
                        foreach (AttributeArgument.NamedArgument na in ca.NamedArguments)
                            if (na.Name == "Skip")
                            {
                                ignoreMessage = ((MetadataExpression.StringExpression)na.Value).Value;
                                return true;
                            }
                        // otherwise give up
                        break;
                    }
                }

                ignoreMessage = null;
                return false;
            }

            public override bool TryGetAssemblySetupTeardownMethods(
                AssemblyEx assembly,
                out Method setup,
                out Method teardown)
            {
                setup = teardown = null;
                return false;
            }

            public override bool TryGetFixtureSetupTeardownMethods(
                TypeEx type,
                out Method fixtureSetup,
                out Method fixtureTeardown,
                out Method testSetup,
                out Method testTeardown
                )
            {
                // xunit supports a default .ctor as setup and Dispose as teardown,
                // which pex supports by default
                testSetup = testTeardown = null;
                fixtureSetup = fixtureTeardown = null;
                return false;
            }

            public override TypeName AssertionExceptionType
            {
                get { return assertException; }
            }
        }
    }
}
